// Author: wsfuyibing <682805@qq.com>
// Date: 2025-04-10

package database

import (
	"context"
	"errors"
	"gorm.io/gorm"
)

type (
	// Dao is an alias of Database Access Object, It's read and write data on
	// database server such as mysql, postgres.
	Dao[T any] struct {
		db  *gorm.DB
		key string
		typ T
	}

	DaoIdField interface {
		PrimaryKeyName() string
	}
)

// NewDao creates a dao instance with given model type and model struct. Data
// in database operated depends on model type.
//
//	type ModelService struct {
//	    Dao *database.Dao[models.Model]
//	}
//
//	func NewModelService(dbs ...*framework.DB) *ModelService {
//	    o := &ModelService{}
//	    o.Dao = database.NewDao[models.ErpSales](models.ErpSales{})
//	    o.Dao.WithConn(dbs...)
//	    o.Dao.WithKey("db")
//	    return o
//	}
func NewDao[T any](t T) *Dao[T] {
	return &Dao[T]{
		typ: t,
		key: defaultKey,
	}
}

// +---------------------------------------------------------------------------+
// | Common operations                                                         |
// +---------------------------------------------------------------------------+

// Master returns a session from master connection.
func (o *Dao[T]) Master(ctx context.Context) (*gorm.DB, error) {
	if o.db != nil {
		return o.db, nil
	}
	return Mgr.GetMasterConnection(ctx)
}

// Slaver returns a session from slaver connection.
func (o *Dao[T]) Slaver(ctx context.Context) (*gorm.DB, error) {
	if o.db != nil {
		return o.db, nil
	}
	return Mgr.GetSlaverConnection(ctx)
}

// WithConn bind a session of database connection.
func (o *Dao[T]) WithConn(dbs ...*gorm.DB) *Dao[T] {
	if len(dbs) > 0 && dbs[0] != nil {
		o.db = dbs[0]
	}
	return o
}

// WithKey bind a connection key for connector.
func (o *Dao[T]) WithKey(k string) *Dao[T] {
	if k != "" {
		o.key = k
	}
	return o
}

// +---------------------------------------------------------------------------+
// | Read / Get one record                                                     |
// +---------------------------------------------------------------------------+

// GetBy returns a model by given condition with key and value. Param k is column
// name and v is column value.
//
//	  // sql: SELECT * FROM `table` WHERE `id` = 10 LIMIT 1
//		 svc.Dao.GetBy(ctx, "id", 10)
func (o *Dao[T]) GetBy(ctx context.Context, k string, v any) (model T, has bool, err error) {
	return o.GetByMap(ctx, map[string]any{k: v})
}

// GetById returns a model by given primary key value.
//
//	  // sql: SELECT * FROM `table` WHERE `id` = 10 LIMIT 1
//		 svc.Dao.GetById(ctx, 10)
func (o *Dao[T]) GetById(ctx context.Context, i any) (model T, has bool, err error) {
	return o.GetByMap(ctx, map[string]any{o.PrimaryKeyId(): i})
}

// GetByMap returns a model by given condition with mapper param.
//
//	// sql: SELECT * FROM `table` WHERE `id` = 10 LIMIT 1
//	svc.Dao.GetByMap(ctx, map[string]any{
//	    "id": 10
//	})
func (o *Dao[T]) GetByMap(ctx context.Context, m map[string]any) (model T, has bool, err error) {
	var db, tx *gorm.DB

	// Open a session from slaver connection.
	if db, err = Mgr.GetSlaverConnection(ctx); err != nil {
		return
	}

	// Init an empty model.
	model = o.typ

	// Send query on database.
	tx = db.Where(m).Take(&model)

	// Compare occurred error for record not found.
	if err = tx.Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			err = nil
		}
		return
	}

	// Record found if affected count is greater than 0.
	has = tx.RowsAffected == 1
	return
}

// GetByStruct returns a model by given condition with model struct.
//
//	// sql: SELECT * FROM `table` WHERE `id` = 10 LIMIT 1
//	svc.Dao.GetByModel(ctx, &models.Model{
//	    Id: 10,
//	})
func (o *Dao[T]) GetByStruct(ctx context.Context, t T) (model T, has bool, err error) {
	var db, tx *gorm.DB

	// Open a session from slaver connection.
	if db, err = Mgr.GetSlaverConnection(ctx); err != nil {
		return
	}

	// Init an empty model.
	model = o.typ

	// Send query on database.
	tx = db.Where(t).Take(&model)

	// Compare occurred error for record not found.
	if err = tx.Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			err = nil
			return
		}
	}

	// Record found if affected count is greater than 0.
	has = tx.RowsAffected == 1
	return
}

// +---------------------------------------------------------------------------+
// | Read / List records.                                                      |
// +---------------------------------------------------------------------------+

// ListByMap returns a list of models by given condition with map param.
//
//	sql: SELECT * FROM `table` WHERE `user_id` = 1
//	svc.Dao.GetByModel(ctx, map[string]any{
//	    "user_id": 1
//	})
func (o *Dao[T]) ListByMap(ctx context.Context, m map[string]any) (list []T, err error) {
	var db, tx *gorm.DB

	// Open a session from slaver connection.
	if db, err = Mgr.GetSlaverConnection(ctx); err != nil {
		return
	}

	// Init an empty model list.
	list = make([]T, 0)

	// Send query on database.
	tx = db.Where(m).Find(&list)

	// Unset list if error occurred or no record found.
	if err = tx.Error; err != nil || tx.RowsAffected == 0 {
		list = nil
	}
	return
}

// ListByStruct returns a list of models by given condition with model struct.
//
//	// sql: SELECT * FROM `table` WHERE `user_id` = 10
//	svc.Dao.GetByModel(ctx, &models.Model{
//	    UserId: 1
//	})
func (o *Dao[T]) ListByStruct(ctx context.Context, t T) (list []T, err error) {
	var db, tx *gorm.DB

	// Open a session from slaver connection.
	if db, err = Mgr.GetSlaverConnection(ctx); err != nil {
		return
	}

	// Init an empty model list.
	list = make([]T, 0)

	// Send query on database.
	tx = db.Where(t).Find(&list)

	// Unset list if error occurred or no record found.
	if err = tx.Error; err != nil || tx.RowsAffected == 0 {
		list = nil
	}
	return
}

// +---------------------------------------------------------------------------+
// | Read / List records with paginator.                                       |
// +---------------------------------------------------------------------------+

// PagingByMap returns a list of models by given condition with map and calculate
// total items in table.
func (o *Dao[T]) PagingByMap(ctx context.Context, m map[string]any, page, size int) (list []T, total int64, err error) {
	var db, tx *gorm.DB

	// Open a session from slaver connection.
	if db, err = Mgr.GetSlaverConnection(ctx); err != nil {
		return
	}

	// Execute matched record count.
	if tx = db.Model(o.typ).Where(m).Count(&total); tx.Error != nil {
		err = tx.Error
		return
	}

	// Return if no matched record.
	if total == 0 {
		return
	}

	// Init an empty model list.
	list = make([]T, 0)

	// Send query on database.
	tx = db.Where(m).Offset((page - 1) * size).Limit(size).Find(&list)

	// Unset list if error occurred or no rows affected.
	if err = tx.Error; err != nil || tx.RowsAffected == 0 {
		list = nil
	}
	return
}

// PagingByStruct returns a list of models by given condition with model struct and
// calculate total items in table.
func (o *Dao[T]) PagingByStruct(ctx context.Context, t T, page, size int) (list []T, total int64, err error) {
	var db, tx *gorm.DB

	// Open a session from slaver connection.
	if db, err = Mgr.GetSlaverConnection(ctx); err != nil {
		return
	}

	// Execute matched record count.
	if tx = db.Model(o.typ).Where(t).Count(&total); tx.Error != nil {
		err = tx.Error
		return
	}

	// Return if no matched record.
	if total == 0 {
		return
	}

	// Init an empty model list.
	list = make([]T, 0)

	// Send query on database.
	tx = db.Where(t).Offset((page - 1) * size).Limit(size).Find(&list)

	// Unset list if error occurred or no rows affected.
	if err = tx.Error; err != nil || tx.RowsAffected == 0 {
		list = nil
	}
	return
}

// +---------------------------------------------------------------------------+
// | Write / Add new record                                                    |
// +---------------------------------------------------------------------------+

// AddByStruct adds a model by given model struct.
//
//	svc.Dao.AddByModel(ctx, &models.Model{
//	    UserId: 1,
//	    ...
//	})
func (o *Dao[T]) AddByStruct(ctx context.Context, t T) (model T, err error) {
	var db, tx *gorm.DB

	// Open a session from master connection.
	if db, err = o.Master(ctx); err != nil {
		return
	}

	// Send query on database.
	tx = db.Create(&t)

	// Render result model if succeed.
	if err = tx.Error; err == nil {
		model = t
	}
	return
}

// +---------------------------------------------------------------------------+
// | Write / Delete records                                                    |
// +---------------------------------------------------------------------------+

// DeleteBy delete records by given condition with key value pairs. Param k is
// column name and v is column value.
//
//	// sql: DELETE FORM `table` WHERE `id` = 1
//	svc.Dao.DeleteBy(ctx, "id", 1)
func (o *Dao[T]) DeleteBy(ctx context.Context, k string, v any) (affects int64, err error) {
	return o.DeleteByMap(ctx, map[string]any{k: v})
}

// DeleteById delete records by given condition with primary key value. Param v is
// an integer value of primary key.
//
//	// sql: DELETE FORM `table` WHERE `id` = 1
//	svc.Dao.DeleteById(ctx, 1)
func (o *Dao[T]) DeleteById(ctx context.Context, v any) (affects int64, err error) {
	return o.DeleteByMap(ctx, map[string]any{o.PrimaryKeyId(): v})
}

// DeleteByMap delete records by given condition with multiple key value pairs.
//
//	// sql: DELETE FORM `table` WHERE `status` = 0 AND `status_type` = "error"
//	svc.Dao.DeleteByMap(ctx, map[string]any{
//	    "status": 0,
//	    "status_type": "error",
//	})
func (o *Dao[T]) DeleteByMap(ctx context.Context, m map[string]any) (affects int64, err error) {
	var db, tx *gorm.DB

	// Open a session from master connection.
	if db, err = o.Master(ctx); err != nil {
		return
	}

	// Send delete request.
	tx = db.Where(m).Delete(o.typ)

	// Calculate affect rows if no error occurred.
	if err = tx.Error; err == nil {
		affects = tx.RowsAffected
	}
	return
}

// DeleteByStruct delete records by given condition with struct definition.
//
//	// sql: DELETE FORM `table` WHERE `status` = 1 AND `status_type` = "error"
//	svc.Dao.DeleteByMap(ctx, models.Model{
//	    "status": 1,
//	    "status_type": "error",
//	})
func (o *Dao[T]) DeleteByStruct(ctx context.Context, t T) (affects int64, err error) {
	var db, tx *gorm.DB

	// Open a session from master connection.
	if db, err = o.Master(ctx); err != nil {
		return
	}

	// Send delete query on database.
	tx = db.Where(t).Delete(o.typ)

	// Calculate affect rows if no error occurred.
	if err = tx.Error; err == nil {
		affects = tx.RowsAffected
	}
	return
}

// +---------------------------------------------------------------------------+
// | Write                                                                     |
// +---------------------------------------------------------------------------+

// UpdateFieldsById updates a record with key value pairs by primary key.
//
//	// sql: UPDATE `table` SET `name` = "test", `age` = 18 WHERE `id` = 10
//	svc.Dao.UpdateMapById(ctx, map[string]any{
//	    "name": "test",
//	    "age": 18,
//	}, 10)
func (o *Dao[T]) UpdateFieldsById(ctx context.Context, fields map[string]any, v any) (affects int64, err error) {
	return o.UpdateFieldsByMap(ctx, fields, map[string]any{
		o.PrimaryKeyId(): v,
	})
}

// UpdateFieldsByMap updates a record with map condition.
//
//	// sql: UPDATE `table` SET `name` = "test", `age` = 18 WHERE `id` = 10 AND `status` = 0
//	svc.Dao.UpdateFieldsByMap(ctx, map[string]any{
//	    "name": "test",
//	    "age": 18,
//	}, map[string]any{
//	    "id": 10,
//	    "status": 0,
//	})
func (o *Dao[T]) UpdateFieldsByMap(ctx context.Context, fields, condition map[string]any) (affects int64, err error) {
	var db, tx *gorm.DB

	// Open a session from master connection.
	if db, err = o.Master(ctx); err != nil {
		return
	}

	// Send update query on database.
	tx = db.Model(o.typ).Where(condition).Updates(fields)

	// Calculate affect rows if no error occurred.
	if err = tx.Error; err == nil {
		affects = tx.RowsAffected
	}
	return
}

func (o *Dao[T]) UpdateByStruct(ctx context.Context, model *T) (affects int64, err error) {
	return
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *Dao[T]) PrimaryKeyId() (id string) {
	var ptr any = &o.typ
	if v, ok := ptr.(DaoIdField); ok {
		if id = v.PrimaryKeyName(); id != "" {
			return
		}
	}
	return "id"
}
